/*
 * Copyright 2011-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0.
 * See `LICENSE` in the project root for license information.
 */

package me.ijleex.platform.uploadfile.util;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 文件加密工具
 *
 * @author liym
 * @since 2019-12-06 13:41 新建
 */
abstract class FileCryptoUtil {

    private static final Logger logger = LoggerFactory.getLogger(FileCryptoUtil.class);

    /**
     * @see com.sun.crypto.provider.ChaCha20Cipher.ChaCha20Poly1305
     */
    private static final String CIPHER_ALGORITHM = "ChaCha20-Poly1305";

    private static final byte[] EMPTY_CONTENT = new byte[0];

    private static final int CIPHER_KEY_LENGTH = 32;
    private static final int CIPHER_IV_LENGTH = 12;

    private FileCryptoUtil() {
    }

    /**
     * 文件加密
     *
     * @param fileContent 文件内容
     * @param fileName 文件名称
     * @param fileType 文件类型
     * @param fileSize 文件大小（Bytes）
     * @return 加密内容
     * @since 2019-12-06 13:50
     */
    public static byte[] encrypt(byte[] fileContent, String fileName, String fileType, long fileSize) {
        if (fileContent == null || fileContent.length == 0) {
            return EMPTY_CONTENT;
        }

        try {
            byte[] key = new byte[CIPHER_KEY_LENGTH];
            byte[] nonce = new byte[CIPHER_IV_LENGTH];
            genKey(fileName, fileType, fileSize, key, nonce);

            return chacha20Poly1305(Cipher.ENCRYPT_MODE, fileContent, key, nonce);
        } catch (Exception e) {
            logger.error("加密文件失败", e);
            return EMPTY_CONTENT;
        }
    }

    /**
     * 文件解密
     *
     * @param fileContent 文件内容（加密）
     * @param fileName 文件名称
     * @param fileType 文件类型
     * @param fileSize 文件大小（Bytes）
     * @return 文件内容
     * @since 2019-12-06 14:07
     */
    public static byte[] decrypt(byte[] fileContent, String fileName, String fileType, long fileSize) {
        if (fileContent == null || fileContent.length == 0) {
            return EMPTY_CONTENT;
        }

        try {
            byte[] key = new byte[CIPHER_KEY_LENGTH];
            byte[] nonce = new byte[CIPHER_IV_LENGTH];
            genKey(fileName, fileType, fileSize, key, nonce);

            return chacha20Poly1305(Cipher.DECRYPT_MODE, fileContent, key, nonce);
        } catch (Exception e) {
            logger.error("解密文件失败，请检查文件参数是否正确", e);
            return EMPTY_CONTENT;
        }
    }

    /**
     * 根据文件名称、类型、大小等属性计算文件密钥
     *
     * @param fileName 文件名称
     * @param fileType 类型
     * @param fileSize 大小
     * @param key 文件的密码
     * @param nonce （伪）唯一随机数
     */
    private static void genKey(String fileName, String fileType, long fileSize, byte[] key, byte[] nonce) {
        byte[] sha256 = DigestUtils.sha256(fileName + ".!!!@" + fileType + ".###=" + fileSize);

        System.arraycopy(sha256, 0, key, 0, CIPHER_KEY_LENGTH);
        System.arraycopy(sha256, 20, nonce, 0, CIPHER_IV_LENGTH);
    }

    /**
     * ChaCha20-Poly1305 加密
     *
     * <p>Java 11 新增加密算法：ChaCha20-Poly1305。</p>
     * <pre>
     *   ChaCha20 是一种流式对称加密算法。
     *   Poly1305 是一种带密码的消息摘要算法。
     *   ChaCha20-Poly1305 是一种流式对称加密算法。
     * </pre>
     */
    private static byte[] chacha20Poly1305(int opmode, byte[] data, byte[] key, byte[] nonce) throws Exception {
        // 密钥的长度必须为 32 字节
        if (key == null || key.length != CIPHER_KEY_LENGTH) {
            throw new IllegalArgumentException("key length must be " + (CIPHER_KEY_LENGTH * 8) + " bits");
        }
        // nonce 长度必须为 12 字节
        if (nonce == null || nonce.length != CIPHER_IV_LENGTH) {
            // ChaCha20-Poly1305 nonce must be 12 bytes in length
            throw new IllegalArgumentException("nonce must be " + CIPHER_IV_LENGTH + " bytes in length");
        }

        SecretKeySpec keySpec = new SecretKeySpec(key, CIPHER_ALGORITHM);
        IvParameterSpec paramSpec = new IvParameterSpec(nonce);

        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        cipher.init(opmode, keySpec, paramSpec);
        return cipher.doFinal(data);
    }

}
